/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.commons.lang;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 *
 * @author <a href=mailto:hedyn@foxmail.com>HeDYn</a>
 */
public final class Zips {

    /**
     * 将指定的zip文件中的内容(不含zip文件名目录)解压缩到descDir目录中
     * @param zipFile 要解压缩的zip文件
     * @param descDir zip文件中的内容要解压缩到的目标目录
     */
    public static void unzip(Path zipFile, Path descDir) throws Exception {
        unzip(zipFile, descDir, false);
    }

    public static void unzipSafeOverwrite(Path zipFile, Path descDir) throws Exception {
        unzip(zipFile, descDir, true);
    }

    /**
     * 将指定的zip文件中的内容(不含zip文件名目录)解压缩到descDir目录中
     * @param zipFile 要解压缩的zip文件
     * @param descDir zip文件中的内容要解压缩到的目标目录
     * @param safeOverwrite 为true时，如果目标文件存在将采用先解压缩到 .temp 文件，再重命名为目标文件的方式
     */
    public static void unzip(Path zipFile, Path descDir, boolean safeOverwrite) throws Exception {
        if (Files.notExists(descDir)) {
            Files.createDirectory(descDir);
        }

        byte[] buf = new byte[Streams.BUFFER_SIZE];
        try (ZipFile zip = new ZipFile(zipFile.toFile())) {
            Enumeration<? extends ZipEntry> entries = zip.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                String zipEntryName = entry.getName();
                Path destFile = descDir.resolve(zipEntryName);
                if (entry.isDirectory()) {
                    Files.createDirectories(destFile);
                } else {
                    Path destFileParentDir = destFile.getParent();
                    // 判断路径是否存在, 不存在则创建文件路径
                    if (Files.notExists(destFileParentDir)) {
                        Files.createDirectories(destFileParentDir);
                    }
                    if (safeOverwrite) {
                        Path tempDestFile = descDir.resolve(zipEntryName + ".temp");
                        try (InputStream is = zip.getInputStream(entry)) {
                            try (OutputStream os = Files.newOutputStream(tempDestFile)) {
                                int len;
                                while ((len = is.read(buf)) > 0) {
                                    os.write(buf, 0, len);
                                }
                            }
                        }
                        try {
                            Files.move(tempDestFile, destFile, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
                        } catch (Exception e) {
                            Files.delete(tempDestFile);
                            throw e;
                        }
                    } else {
                        try (InputStream is = zip.getInputStream(entry)) {
                            try (OutputStream os = Files.newOutputStream(destFile)) {
                                int len;
                                while ((len = is.read(buf)) > 0) {
                                    os.write(buf, 0, len);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    public static ZipCompress keepStructure() {
        return new ZipCompressImpl(true);
    }

    public static ZipCompress notStructure() {
        return new ZipCompressImpl(true);
    }

    /**
     * @author <a href=mailto:hedyn@foxmail.com>HeDYn</a>
     */
    public interface ZipCompress {

        ZipCompress snapshoot();

        ZipCompress level(int level);

        ZipCompress bestSpeed();

        ZipCompress bestCompression();

        ZipCompress addFile(String name, File file);

        ZipCompress addFile(File... files);

        ZipCompress addFiles(List<File> files);

        boolean isEmpty();

        void compress(OutputStream out) throws Exception;

    }

    /**
     * @author <a href=mailto:hedyn@foxmail.com>HeDYn</a>
     */
    private static class ZipCompressImpl implements ZipCompress {

        private final boolean keepStructure;
        private final Map<File, String> fileMap;
        private boolean snapshoot;
        private Integer level;

        private ZipCompressImpl(boolean keepStructure) {
            this.keepStructure = keepStructure;
            this.fileMap = new LinkedHashMap<>();
        }

        @Override
        public ZipCompress snapshoot() {
            this.snapshoot = true;
            return this;
        }

        @Override
        public ZipCompress level(int level) {
            if (level < 0 || level > 9) {
                throw new IllegalArgumentException("invalid compression level");
            }
            this.level = level;
            return this;
        }

        @Override
        public ZipCompress bestSpeed() {
            this.level = Deflater.BEST_SPEED;
            return this;
        }

        @Override
        public ZipCompress bestCompression() {
            this.level = Deflater.BEST_COMPRESSION;
            return this;
        }

        @Override
        public ZipCompress addFile(String name, File file) {
            if (file.exists()) {
                fileMap.put(file, name);
            }
            return this;
        }

        @Override
        public ZipCompress addFile(File... files) {
            Checks.verifyNotEmpty(files, "files");
            return addFiles(Arrays.asList(files));
        }

        @Override
        public ZipCompress addFiles(List<File> files) {
            Checks.verifyNotEmpty(files, "files");
            for (File file : files) {
                if (file.exists()) {
                    fileMap.put(file, file.getName());
                }
            }
            return this;
        }

        @Override
        public boolean isEmpty() {
            return fileMap.isEmpty();
        }

        @Override
        public void compress(OutputStream out) throws Exception {
            try (ZipOutputStream zos = new ZipOutputStream(out)) {
                if (level != null) {
                    zos.setLevel(level);
                }
                for (Map.Entry<File, String> entry : fileMap.entrySet()) {
                    doCompress(entry.getKey(), zos, entry.getValue(), keepStructure, snapshoot);
                }
                zos.finish();
            }
        }

        private static void doCompress(File sourceFile,
                                       ZipOutputStream zos,
                                       String name,
                                       boolean keepStructure,
                                       boolean snapshoot) throws Exception {
            if (sourceFile.isFile()) {
                zos.putNextEntry(new ZipEntry(name));
                Streams.transfer(sourceFile, zos, snapshoot);
                zos.closeEntry();
            } else if (sourceFile.isDirectory()) {
                File[] listFiles = sourceFile.listFiles();
                if (listFiles == null || listFiles.length == 0) {
                    // 需要保留原来文件结构时,需要对空文件夹进行处理
                    if (keepStructure) {
                        zos.putNextEntry(new ZipEntry(name + "/"));
                        zos.closeEntry();
                    }

                } else {
                    for (File file : listFiles) {
                        if (keepStructure) {
                            // 保留原来文件结构时前面需要带上父文件夹名字
                            doCompress(file, zos, name + "/" + file.getName(), keepStructure, snapshoot);
                        } else {
                            doCompress(file, zos, file.getName(), keepStructure, snapshoot);
                        }
                    }
                }
            }
        }
    }
}
